package com.zenithmind.disk.service.impl;

import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zenithmind.disk.feign.UserFeignClient;
import com.zenithmind.disk.mapper.FilesMapper;
import com.zenithmind.disk.pojo.domain.Files;
import com.zenithmind.disk.pojo.dto.FileDTO;
import com.zenithmind.disk.pojo.vo.FileVO;
import com.zenithmind.disk.pojo.vo.UsersVO;
import com.zenithmind.disk.service.FilesService;
import com.zenithmind.common.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import com.zenithmind.common.constant.CacheConstants;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.beans.factory.annotation.Qualifier;
import jakarta.servlet.http.HttpServletResponse;
import com.zenithmind.disk.pojo.query.FileQuery;
import com.zenithmind.common.result.PageResult;
import org.apache.commons.lang3.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.stream.Collectors;
import java.util.Map;
import java.util.HashMap;
import com.zenithmind.common.annotation.DataCache;
import com.zenithmind.common.annotation.DataCacheEvict;
import com.zenithmind.common.security.SecurityUtils;

@Slf4j
@Service
public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements FilesService {
    @Autowired
    @Qualifier("com.zenithmind.disk.feign.UserFeignClient")
    private UserFeignClient userFeignClient;

    @Value("${file.upload.path}")
    private String uploadPath;

    @Autowired
    private FilesMapper fileMapper;
    
    // 缓存前缀常量定义
    private static final String CACHE_FILES_PREFIX = "files";
    private static final String CACHE_USER_FILES_PREFIX = "user:files";

    /**
     * 上传文件方法
     *
     * @param file     要上传的文件
     * @param userId   用户ID
     * @param parentId 文件父目录ID
     * @return 返回文件信息对象
     * @throws RuntimeException 如果用户不存在、存储空间不足、创建文件夹失败等情况下抛出运行时异常
     */
    @Override
    @Transactional // 开启事务管理
    @DataCacheEvict(prefix = CACHE_FILES_PREFIX, key = "#result.id")
    public FileVO uploadFile(MultipartFile file, String userId, String parentId) {
        try {
            // 处理root标记，将parentId值为"root"的情况转换为null
            if ("root".equals(parentId)) {
                parentId = null;
            }
            
            //获取文件名
            String originalFilename = file.getOriginalFilename();
            //获取文件大小
            long fileSize = file.getSize();
            //获取文件类型
            String fileType = file.getContentType();

            // 检查用户存储空间是否足够 (Use Feign Client)
            Result<UsersVO> userResult = userFeignClient.getUserById(userId);
            if (userResult == null || !userResult.isSuccess() || userResult.getData() == null) { 
                log.error("Failed to get user info for upload: userId={}. Reason: {} - {}", 
                          userId, 
                          userResult != null ? userResult.getCode() : "NULL_RESULT", 
                          userResult != null ? userResult.getMsg() : "Feign client returned null");
                throw new RuntimeException("获取用户信息失败或用户不存在");
            }
            UsersVO user = userResult.getData();

            if (user.getStorageUsed() + fileSize > user.getStorageLimit()) {
                throw new RuntimeException("存储空间不足");
            }

            //生成文件MD5码
            String fileMd5 = DigestUtil.md5Hex(file.getInputStream());
            //检查文件是否已存在
            List<Files> existingFilesList = list(new QueryWrapper<Files>().eq("file_md5", fileMd5));
            Files existingFileRecord = existingFilesList.isEmpty() ? null : existingFilesList.get(0);
            
            // 查找是否已存在相同用户、相同父目录、相同文件名的记录 (Status = 1)
            LambdaQueryWrapper<Files> duplicateCheckWrapper = new LambdaQueryWrapper<>();
            duplicateCheckWrapper.eq(Files::getUserId, userId)
                    .eq(Files::getFileName, originalFilename)
                    .eq(Files::getStatus, 1); // Only check against active files
            if (parentId == null) {
                 duplicateCheckWrapper.isNull(Files::getParentId);
            } else {
                 duplicateCheckWrapper.eq(Files::getParentId, parentId);
            }
            boolean isDuplicate = fileMapper.selectCount(duplicateCheckWrapper) > 0;

            String finalFileName = originalFilename;
            // 如果存在重复文件名，自动重命名
            if (isDuplicate) {
                 finalFileName = generateUniqueFilename(userId, parentId, originalFilename);
            }
            
            // 使用年月日目录结构
            String relativePath = "/files/" + LocalDateTime.now().getYear() + 
                              "/" + LocalDateTime.now().getMonthValue() + 
                              "/" + LocalDateTime.now().getDayOfMonth();
                             
            // 生成唯一的文件名，避免直接使用可能包含特殊字符的原始文件名
            String uniqueDiskFileName = IdUtil.fastSimpleUUID() + getFileExtension(finalFileName);
            String fullDiskPath = relativePath + "/" + uniqueDiskFileName;
            String absoluteDiskPath = uploadPath + fullDiskPath;
            
            // 确保文件路径存在
            File directory = new File(uploadPath + relativePath);
            if (!directory.exists()) {
                boolean mkdirs = directory.mkdirs();
                if (!mkdirs) {
                    throw new RuntimeException("创建文件夹失败");
                }
            }
            
            // 保存文件到磁盘
            file.transferTo(new File(absoluteDiskPath));
            
            // 保存文件信息进数据库
            Files newFile = new Files();
            newFile.setUserId(userId);
            newFile.setParentId(parentId);
            newFile.setFileName(finalFileName); // Use potentially renamed filename
            newFile.setFileType(fileType);
            newFile.setFileSize(fileSize);
            newFile.setFilePath(fullDiskPath); // Store relative path
            newFile.setIsDirectory(0);
            newFile.setFileMd5(fileMd5);
            newFile.setStatus(1);
            this.save(newFile);
            
            // 更新用户的存储空间 (Use Feign Client)
            Result<Void> updateResult = userFeignClient.updateStorageUsed(userId, fileSize);
            if (updateResult == null || !updateResult.isSuccess()) { 
                log.error("Failed to update user storage after upload: userId={}, fileSize={}. Reason: {} - {}", 
                          userId, fileSize, 
                          updateResult != null ? updateResult.getCode() : "NULL_RESULT", 
                          updateResult != null ? updateResult.getMsg() : "Feign client returned null");
                throw new RuntimeException("更新用户存储空间失败");
            }

            //返回文件信息
            FileVO fileVO = new FileVO();
            BeanUtils.copyProperties(newFile, fileVO);
            return fileVO;
        } catch (IOException e) {
            log.error("Error uploading file for user {}: {}", userId, e.getMessage(), e);
            throw new RuntimeException("文件上传失败", e);
        }
    }

    /**
     * 创建文件夹
     * 此方法用于在系统中创建一个新的文件夹它首先检查在相同的父目录下是否已经存在同名的文件夹，
     * 如果存在，则抛出运行时异常如果不存在，则创建一个新的文件夹对象，将其保存到数据库中，
     * 并返回表示该文件夹的文件视图对象（FileVO）
     *
     * @param fileDTO 包含创建文件夹所需信息的数据传输对象
     * @return 返回表示新创建文件夹的视图对象
     * @throws RuntimeException 如果同级目录下已存在同名文件夹，则抛出此异常
     */
    @Override
    @Transactional
    public FileVO createFolder(FileDTO fileDTO) {
        // 处理root标记，将parentId值为"root"的情况转换为null
        if ("root".equals(fileDTO.getParentId())) {
            fileDTO.setParentId(null);
        }

        // 检查用户是否存在 (Optional: Depends if user ID is validated upstream)
        // Result<UsersVO> userResult = userFeignClient.getUserById(fileDTO.getUserId());
        // if (userResult == null || !userResult.isSuccess() || userResult.getData() == null) {
        //     throw new RuntimeException("获取用户信息失败或用户不存在");
        // }

        // 检查同名文件夹
        LambdaQueryWrapper<Files> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Files::getUserId, fileDTO.getUserId())
                .eq(Files::getFileName, fileDTO.getFileName())
                .eq(Files::getIsDirectory, 1)
                .eq(Files::getStatus, 1); // Only check active folders
        if (fileDTO.getParentId() == null) {
            queryWrapper.isNull(Files::getParentId);
        } else {
            queryWrapper.eq(Files::getParentId, fileDTO.getParentId());
        }

        if (fileMapper.selectCount(queryWrapper) > 0) {
            throw new RuntimeException("已存在同名文件夹");
        }

        Files folder = new Files();
        folder.setUserId(fileDTO.getUserId());
        folder.setParentId(fileDTO.getParentId());
        folder.setFileName(fileDTO.getFileName());
        folder.setIsDirectory(1);
        folder.setStatus(1);
        folder.setFileSize(0L); // Folder size is typically 0

        fileMapper.insert(folder);

        FileVO fileVO = new FileVO();
        BeanUtils.copyProperties(folder, fileVO);
        return fileVO;
    }

    /**
     * 根据用户ID和父文件夹ID列出用户文件
     * 此方法查询数据库中属于特定用户且位于特定父文件夹下的所有文件，并将它们转换为FileVO对象列表
     *
     * @param userId   用户ID，用于筛选属于该用户的文件
     * @param parentId 父文件夹ID，用于筛选位于该父文件夹下的文件
     * @return 包含用户文件信息的FileVO对象列表
     */
    @Override
    public List<FileVO> listFiles(String userId, String parentId) {
        // 处理root标记，将parentId值为"root"的情况转换为null
        if ("root".equals(parentId)) {
            parentId = null;
        }
        
        // 创建查询条件构建器
        LambdaQueryWrapper<Files> queryWrapper = new LambdaQueryWrapper<>();
        // 设置查询条件：根据用户ID、父文件夹ID和文件状态进行筛选
        queryWrapper.eq(Files::getUserId, userId);
        // 父目录ID可能为null(根目录)，需要特殊处理
        if (parentId == null) {
            queryWrapper.isNull(Files::getParentId);
        } else {
            queryWrapper.eq(Files::getParentId, parentId);
        }
        queryWrapper.eq(Files::getStatus, 1);
        // 执行查询，获取文件列表
        List<Files> files = fileMapper.selectList(queryWrapper);

        // 将查询到的文件数据转换为FileVO对象列表
        List<FileVO> fileVOList = files.stream().map(fileData -> {
            // 创建一个新的FileVO对象
            FileVO fileVO = new FileVO();
            // 将文件数据从Files对象复制到FileVO对象
            BeanUtils.copyProperties(fileData, fileVO);
            // 设置isDirectory为布尔值类型
            fileVO.setIsDirectory(fileData.getIsDirectory() != null && fileData.getIsDirectory() == 1);
            // 返回转换后的FileVO对象
            return fileVO;
        }).toList();
        // 返回FileVO对象列表
        return fileVOList;
    }

    /**
     * 重命名文件
     *
     * @param fileDTO 包含文件信息的数据传输对象，包括文件ID、用户ID、新文件名等
     * @return 返回更新后的文件信息，以FileVO对象形式返回
     * @throws RuntimeException 如果文件不存在、用户无权限操作或同级目录下已存在同名文件，则抛出运行时异常
     */
    @Override
    @Transactional
    public FileVO renameFile(FileDTO fileDTO) {
        Files file = fileMapper.selectById(fileDTO.getId());
        if (file == null || file.getStatus() != 1) {
             throw new RuntimeException("文件或文件夹不存在");
        }
        // Check user permission
        if (!file.getUserId().equals(fileDTO.getUserId())) {
            throw new RuntimeException("无权限操作");
        }
        
        // Check for duplicate name in the same directory
        LambdaQueryWrapper<Files> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Files::getUserId, fileDTO.getUserId())
            .eq(Files::getFileName, fileDTO.getFileName())
            .eq(Files::getIsDirectory, file.getIsDirectory())
            .eq(Files::getStatus, 1)
            .ne(Files::getId, fileDTO.getId()); // Exclude itself
        if (file.getParentId() == null) {
            queryWrapper.isNull(Files::getParentId);
        } else {
            queryWrapper.eq(Files::getParentId, file.getParentId());
        }
        if (fileMapper.selectCount(queryWrapper) > 0) {
            throw new RuntimeException("目标位置已存在同名文件或文件夹");
        }

        file.setFileName(fileDTO.getFileName());
        fileMapper.updateById(file);

        FileVO fileVO = new FileVO();
        BeanUtils.copyProperties(file, fileVO);
        return fileVO;
    }

    /**
     * 移动文件到指定的目标文件夹
     *
     * @param fileId   要移动的文件的ID
     * @param targetId 目标文件夹的ID
     * @param userId   用户ID，用于权限验证
     * @return 文件移动成功返回true，否则返回false
     * @throws RuntimeException 如果文件不存在、用户无权限操作、目标文件夹不存在、目标文件夹为移动文件夹的子文件夹或目标文件夹中已存在同名文件
     */
    @Override
    @Transactional
    public boolean moveFile(String fileId, String targetId, String userId) {
        Files fileToMove = fileMapper.selectById(fileId);
        if (fileToMove == null || fileToMove.getStatus() != 1) {
            throw new RuntimeException("要移动的文件或文件夹不存在");
        }
        if (!fileToMove.getUserId().equals(userId)) {
            throw new RuntimeException("无权限操作");
        }
        
        String targetParentId = "root".equals(targetId) ? null : targetId;
        
        // Check if target is valid (exists, is a directory, belongs to the user, and is not the file itself or a child)
        if (targetParentId != null) {
            Files targetFolder = fileMapper.selectById(targetParentId);
            if (targetFolder == null || targetFolder.getStatus() != 1 || targetFolder.getIsDirectory() != 1) {
                throw new RuntimeException("目标文件夹不存在");
            }
            if (!targetFolder.getUserId().equals(userId)) {
                throw new RuntimeException("无权限移动到目标文件夹");
            }
             // Prevent moving a folder into itself or its children
            if (fileToMove.getIsDirectory() == 1 && isMovingIntoSelfOrChild(fileId, targetParentId)) {
                throw new RuntimeException("不能将文件夹移动到自身或其子文件夹中");
            }
        }
        
        // Check for duplicate name in the target directory
        LambdaQueryWrapper<Files> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Files::getUserId, userId)
            .eq(Files::getFileName, fileToMove.getFileName())
            .eq(Files::getIsDirectory, fileToMove.getIsDirectory())
            .eq(Files::getStatus, 1)
            .ne(Files::getId, fileId); // Exclude itself in case target is current parent
        if (targetParentId == null) {
            queryWrapper.isNull(Files::getParentId);
        } else {
            queryWrapper.eq(Files::getParentId, targetParentId);
        }
        if (fileMapper.selectCount(queryWrapper) > 0) {
             throw new RuntimeException("目标文件夹已存在同名文件或文件夹");
        }

        // Update parent ID
        fileToMove.setParentId(targetParentId);
        int updatedRows = fileMapper.updateById(fileToMove);
        return updatedRows > 0;
    }

    /**
     * Helper method to check if a folder is being moved into itself or one of its descendants.
     */
    private boolean isMovingIntoSelfOrChild(String folderToMoveId, String targetParentId) {
        if (folderToMoveId.equals(targetParentId)) {
            return true; // Moving into itself
        }
        
        String currentParentId = targetParentId;
        while (currentParentId != null) {
            Files parent = fileMapper.selectById(currentParentId);
            if (parent == null) {
                // Should not happen in consistent data, but break loop to be safe
                break; 
            }
            if (parent.getId().equals(folderToMoveId)) {
                return true; // Target is a child of the folder being moved
            }
            currentParentId = parent.getParentId();
        }
        return false;
    }

    /**
     * 删除文件或文件夹
     *
     * @param fileId 文件或文件夹的ID
     * @param userId 用户的ID
     * @return 如果删除成功返回true，否则抛出异常
     * @throws RuntimeException 如果文件不存在或用户无权限操作
     */
    @Override
    @Transactional
    @DataCacheEvict(prefix = CACHE_FILES_PREFIX, key = "#fileId")
    public boolean deleteFile(String fileId, String userId) {
        Files file = fileMapper.selectById(fileId);
        if (file == null) {
            log.warn("Attempted to delete non-existent file: fileId={}, userId={}", fileId, userId);
            return true; // Or throw exception? File already gone.
        }

        if (!file.getUserId().equals(userId)) {
            throw new RuntimeException("用户无权限操作");
        }
        
        // Calculate size reduction *before* marking as deleted
        long sizeToDecrement = 0;
        if (file.getIsDirectory() == 1) {
            sizeToDecrement = calculateDirectorySizeRecursive(fileId, userId, true); // Include folder itself if it has size
        } else if (file.getStatus() == 1) { // Only count active file size
             sizeToDecrement = file.getFileSize();
        }

        // Recursively mark file/folder and contents as deleted (status = 0)
        deleteFileOrFolderRecursive(fileId, userId);

        // Update user storage (Use Feign Client)
        if (sizeToDecrement > 0) {
             Result<Void> updateResult = userFeignClient.updateStorageUsed(userId, -sizeToDecrement);
             if (updateResult == null || !updateResult.isSuccess()) { 
                 log.error("Failed to update user storage after delete: userId={}, sizeDecrement={}. Reason: {} - {}", 
                           userId, -sizeToDecrement, 
                           updateResult != null ? updateResult.getCode() : "NULL_RESULT", 
                           updateResult != null ? updateResult.getMsg() : "Feign client returned null");
                 throw new RuntimeException("更新用户存储空间失败"); 
             }
        }

        return true;
    }

    /**
     * Recursively marks a file or folder and its contents as deleted (status = 0).
     */
    private void deleteFileOrFolderRecursive(String fileId, String userId) {
        Files file = fileMapper.selectById(fileId);
        if (file == null || !file.getUserId().equals(userId)) {
            // File doesn't exist or user doesn't have permission, skip
            return; 
        }

        // Mark the current file/folder as deleted
        if (file.getStatus() == 1) { // Only update if it's currently active
            file.setStatus(0); 
            fileMapper.updateById(file);
            log.info("Marked file/folder as deleted: id={}, name={}, userId={}", file.getId(), file.getFileName(), userId);
        }

        // If it's a directory, recursively delete its contents
        if (file.getIsDirectory() == 1) {
            LambdaQueryWrapper<Files> childrenWrapper = new LambdaQueryWrapper<>();
            childrenWrapper.eq(Files::getParentId, fileId)
                           .eq(Files::getUserId, userId); // Ensure we only affect the user's files
            List<Files> children = fileMapper.selectList(childrenWrapper);
            for (Files child : children) {
                deleteFileOrFolderRecursive(child.getId(), userId);
            }
        }
    }

    /**
     * Recursively calculates the total size of a directory and its contents.
     */
    private long calculateDirectorySizeRecursive(String directoryId, String userId, boolean includeSelf) {
        long totalSize = 0;
        Files directory = fileMapper.selectById(directoryId);

        if (directory == null || !directory.getUserId().equals(userId) || directory.getIsDirectory() != 1) {
            return 0; // Not a valid directory for this user
        }
        
        // Include directory's own size if specified (though usually 0)
        if (includeSelf && directory.getStatus() == 1) { 
             totalSize += directory.getFileSize();
        }

        LambdaQueryWrapper<Files> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Files::getParentId, directoryId)
                    .eq(Files::getUserId, userId)
                    .eq(Files::getStatus, 1); // Only count active files/folders
        List<Files> children = fileMapper.selectList(queryWrapper);

        for (Files child : children) {
            if (child.getIsDirectory() == 1) {
                totalSize += calculateDirectorySizeRecursive(child.getId(), userId, false); // Don't include subfolder size itself, just contents
            } else {
                totalSize += child.getFileSize();
            }
        }
        return totalSize;
    }

    /**
     * 获取文件的物理路径，用于删除操作，不检查文件状态
     *
     * @param fileId 文件ID，用于定位文件
     * @param userId 用户ID，用于权限验证
     * @return 文件的上传路径，如果文件不存在或用户无权限访问，将抛出异常
     * @throws RuntimeException 如果文件不存在或用户无权限访问，将抛出运行时异常
     */
    @Override
    public String getFilePathForDelete(String fileId, String userId) {
         Files file = fileMapper.selectById(fileId);
         if (file == null) {
             // Maybe return null or log warning, file doesn't exist
             return null;
         }
         // Permission check maybe not strictly needed if only used internally after deleteFile check?
         // if (!file.getUserId().equals(userId)) {
         //     throw new RuntimeException("无权限访问该文件");
         // }
         // Return absolute path even if status is not 1, as it's for physical deletion
         return uploadPath + file.getFilePath();
    }

    /**
     * 根据文件ID获取文件信息
     * <p>
     * 此方法首先根据提供的文件ID调用`getById`方法以获取文件的详细信息，
     * 然后将获取到的文件信息复制到FileVO对象中，并返回该对象
     *
     * @param fileId 文件ID，用于唯一标识一个文件
     * @return FileVO 包含文件详细信息的文件视图对象
     */
    @Override
    @DataCache(prefix = CACHE_FILES_PREFIX, key = "#fileId")
    public FileVO getFileInfo(String fileId) {
        // 通过文件ID获取文件实体对象
        Files files = this.getById(fileId);
        // 创建一个文件视图对象
        FileVO fileVO = new FileVO();
        // 将文件实体对象的属性复制到文件视图对象中
        BeanUtils.copyProperties(files, fileVO);
        
        // 显式设置isDirectory属性，防止类型不匹配问题
        if (files.getIsDirectory() != null) {
            fileVO.setIsDirectory(files.getIsDirectory() == 1);
        } else {
            fileVO.setIsDirectory(false); // 默认为false，避免空指针
        }
        
        // 返回文件视图对象
        return fileVO;
    }

    /**
     * 获取文件的物理路径
     *
     * @param fileId 文件ID，用于定位文件
     * @param userId 用户ID，用于权限验证
     * @return 文件的上传路径，如果文件不存在、无效或用户无权限访问，将抛出异常
     * @throws RuntimeException 如果文件不存在或用户无权限访问，将抛出运行时异常
     */
    @Override
    public String getFilePath(String fileId, String userId) {
        Files file = fileMapper.selectById(fileId);
        if (file == null || file.getStatus() != 1) {
            throw new RuntimeException("文件不存在");
        }
        if (!file.getUserId().equals(userId)) {
            throw new RuntimeException("无权限访问该文件");
        }
        // Return absolute path for download/access
        return uploadPath + file.getFilePath(); 
    }

    /**
     * 下载文件
     * @param fileId 文件ID
     * @param userId 用户ID
     * @return 文件物理路径
     */
    @Override
    public String downloadFile(String fileId, String userId) {
         // Simply returns the path, validation happens in getFilePath or controller
         return getFilePath(fileId, userId); 
    }
    
    /**
     * 递归添加文件夹内容到ZIP
     *
     * @param folderId 文件夹ID
     * @param folderPath 文件夹在ZIP中的路径
     * @param userId 用户ID
     * @param zipOut ZIP输出流
     * @throws IOException IO异常
     */
    @Override
    public void addFolderToZip(String folderId, String folderPath, String userId, ZipOutputStream zipOut) throws IOException {
        LambdaQueryWrapper<Files> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Files::getParentId, folderId)
                    .eq(Files::getUserId, userId)
                    .eq(Files::getStatus, 1); // Only zip active files
        List<Files> filesList = fileMapper.selectList(queryWrapper);

        for (Files file : filesList) {
            String entryPath = folderPath + "/" + file.getFileName();
            if (file.getIsDirectory() == 1) {
                // Add directory entry
                ZipEntry folderEntry = new ZipEntry(entryPath + "/");
                zipOut.putNextEntry(folderEntry);
                zipOut.closeEntry();
                // Recurse into subdirectory
                addFolderToZip(file.getId(), entryPath, userId, zipOut);
            } else {
                // Add file entry
                String absoluteFilePath = uploadPath + file.getFilePath(); // Use stored relative path
                File fileToZip = new File(absoluteFilePath);
                if (fileToZip.exists()) {
                    ZipEntry fileEntry = new ZipEntry(entryPath);
                    zipOut.putNextEntry(fileEntry);
                    try (FileInputStream fis = new FileInputStream(fileToZip)) {
                        byte[] buffer = new byte[8192];
                        int length;
                        while ((length = fis.read(buffer)) > 0) {
                            zipOut.write(buffer, 0, length);
                        }
                    }
                    zipOut.closeEntry();
                } else {
                    log.warn("File referenced in DB not found on disk: {}", absoluteFilePath);
                }
            }
        }
    }

    /**
     * Helper method to generate a unique filename if a duplicate exists.
     */
    private String generateUniqueFilename(String userId, String parentId, String originalFilename) {
        String baseName = getFileNameWithoutExtension(originalFilename);
        String extension = getFileExtension(originalFilename);
        int count = 1;
        String newFilename;
        
        LambdaQueryWrapper<Files> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Files::getUserId, userId)
                    .eq(Files::getStatus, 1);
        if (parentId == null) {
            queryWrapper.isNull(Files::getParentId);
        } else {
            queryWrapper.eq(Files::getParentId, parentId);
        }
        
        do {
            newFilename = baseName + "(" + count + ")" + extension;
            queryWrapper.eq(Files::getFileName, newFilename);
            count++;
        } while (fileMapper.selectCount(queryWrapper) > 0);
        
        return newFilename;
    }

    private String getFileNameWithoutExtension(String filename) {
        int dotIndex = filename.lastIndexOf('.');
        return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
    }

    private String getFileExtension(String filename) {
        int dotIndex = filename.lastIndexOf('.');
        return (dotIndex == -1) ? "" : filename.substring(dotIndex);
    }

    /**
     * 下载目录为zip文件
     * @param folderId 文件夹ID
     * @param response HTTP响应对象
     * @throws IOException 如果发生I/O错误
     */
    @Override
    public void downloadDirectory(String folderId, HttpServletResponse response) throws IOException {
        // 获取文件夹信息
        Files folder = getById(folderId);
        if (folder == null || folder.getStatus() != 1) {
            throw new RuntimeException("文件夹不存在或已删除");
        }

        // 设置响应头
        String zipFileName = folder.getFileName() + ".zip";
        response.setContentType("application/zip");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + 
                           java.net.URLEncoder.encode(zipFileName, java.nio.charset.StandardCharsets.UTF_8));
        
        // 创建ZIP输出流
        try (java.util.zip.ZipOutputStream zipOut = new java.util.zip.ZipOutputStream(response.getOutputStream())) {
            // 递归添加文件夹内容到ZIP
            addFolderToZip(folder.getId(), folder.getFileName(), folder.getUserId(), zipOut);
        }
    }

    /**
     * 分页查询文件列表
     *
     * @param fileQuery 文件查询对象，包含查询参数和分页信息
     * @return 返回分页查询结果，包含文件列表和分页相关数据
     */
    @Override
    @DataCache(prefix = CACHE_USER_FILES_PREFIX, key = "#fileQuery.userId + ':' + #fileQuery.parentId + ':page' + #fileQuery.current", 
               expireTime = CacheConstants.TTL_SECONDS_DEFAULT)
    public PageResult<FileVO> filePage(FileQuery fileQuery) {
        Page<Files> page = fileQuery.toPage();

        Page<Files> filesPage = lambdaQuery()
            .like(StringUtils.isNotBlank(fileQuery.getFileName()), Files::getFileName, fileQuery.getFileName())
            .eq(StringUtils.isNotBlank(fileQuery.getUserId()), Files::getUserId, fileQuery.getUserId())
            .eq(StringUtils.isNotBlank(fileQuery.getParentId()), Files::getParentId, fileQuery.getParentId())
            .eq(StringUtils.isNotBlank(fileQuery.getFileType()), Files::getFileType, fileQuery.getFileType())
            .eq(fileQuery.getIsDirectory() != null, Files::getIsDirectory, fileQuery.getIsDirectory())
            .eq(fileQuery.getStatus() != null, Files::getStatus, fileQuery.getStatus())
            .orderByDesc(Files::getUpdateTime)
            .page(page);

        // 转换为VO对象
        List<FileVO> fileVOList = filesPage.getRecords().stream().map(file -> {
            FileVO fileVO = new FileVO();
            BeanUtils.copyProperties(file, fileVO);
            return fileVO;
        }).collect(Collectors.toList());

        return new PageResult<>(fileVOList, filesPage.getTotal(), filesPage.getCurrent(), filesPage.getSize());
    }

    /**
     * 批量删除文件-管理员
     *
     * @param ids 文件ID列表
     * @return 是否成功
     */
    @Override
    @Transactional
    public boolean deleteBatchFiles(List<String> ids) {
        if (ids == null || ids.isEmpty()) {
            return true;
        }

        // 查询要删除的文件
        List<Files> files = listByIds(ids);
        if (files.isEmpty()) {
            return true;
        }

        // 按用户ID分组计算存储空间变化
        Map<String, Long> userStorageChanges = new HashMap<>();
        
        for (Files file : files) {
            if (file.getStatus() == 0) {
                // 文件已被删除，跳过
                continue;
            }
            
            // 计算存储空间变化
            final long sizeToDecrement = file.getIsDirectory() == 1 
                ? calculateDirectorySizeRecursive(file.getId(), file.getUserId(), true)
                : file.getFileSize();
            
            // 累加到用户的存储空间变化中
            userStorageChanges.compute(file.getUserId(), (key, value) -> 
                (value == null) ? sizeToDecrement : value + sizeToDecrement);
            
            // 递归标记为删除状态
            deleteFileOrFolderRecursive(file.getId(), file.getUserId());
        }
        
        // 更新用户存储空间
        for (Map.Entry<String, Long> entry : userStorageChanges.entrySet()) {
            if (entry.getValue() > 0) {
                Result<Void> updateResult = userFeignClient.updateStorageUsed(entry.getKey(), -entry.getValue());
                if (updateResult == null || !updateResult.isSuccess()) {
                    log.error("Failed to update user storage after batch delete: userId={}, sizeDecrement={}",
                            entry.getKey(), -entry.getValue());
                }
            }
        }

        return true;
    }

    /**
     * 管理员添加或修改文件信息
     *
     * @param file 文件信息
     * @return 文件信息
     */
    @Override
    @Transactional
    @DataCacheEvict(prefix = CACHE_FILES_PREFIX, key = "#file.id")
    public FileVO addOrUpdateFileByAdmin(Files file) {
        // 如果是修改操作
        if (StringUtils.isNotBlank(file.getId())) {
            Files existingFile = getById(file.getId());
            if (existingFile == null) {
                throw new RuntimeException("文件不存在");
            }
            
            // 保存修改后的文件信息，保持不可修改字段不变
            file.setFilePath(existingFile.getFilePath());
            file.setFileSize(existingFile.getFileSize());
            file.setFileType(existingFile.getFileType());
            file.setFileMd5(existingFile.getFileMd5());
            updateById(file);
        } else {
            // 添加操作仅允许创建文件夹
            if (file.getIsDirectory() != 1) {
                throw new RuntimeException("管理员只能创建文件夹，不能直接创建文件");
            }
            save(file);
        }
        
        // 返回更新后的文件信息
        FileVO fileVO = new FileVO();
        BeanUtils.copyProperties(getById(file.getId()), fileVO);
        return fileVO;
    }
}
